package indi.mozping.helper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.util.SafeEncoder;

import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Random;

/**
 * @author by mozping
 * @Classname RedisDistributeLock
 * @Description 分布式锁Redis实现类
 * @Date 2019/11/13 14:52
 */
public class RedisDistributeLock implements DistributeLock {

    private static final Logger log = LoggerFactory.getLogger(RedisDistributeLock.class);

    private static final String
            UNLOCK_LUA_SCRIPT =
            "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    private static final DefaultRedisScript<List> DEFAULT_REDIS_SCRIPT;

    static {
        DEFAULT_REDIS_SCRIPT = new DefaultRedisScript<List>();
        DEFAULT_REDIS_SCRIPT.setResultType(List.class);
        DEFAULT_REDIS_SCRIPT.setScriptText(UNLOCK_LUA_SCRIPT);
    }

    private static final Random RANDOM = new Random();

    @Autowired
    private RedisTemplate redisTemplate;

    @Value("${distribute.lock.expireTime:5000}")
    long defaultExpireTime;

    /**
     * @param key   加锁 key
     * @param value 加锁 value
     * @Description: 加锁方法，一直阻塞尝试加锁直到成功
     * @date 2019/11/13 16:10
     * @author by mozping
     */
    public boolean lock(String key, String value) {
        return lock(key, value, defaultExpireTime);
    }

    /**
     * @param key        加锁 key
     * @param value      加锁 value
     * @param exireptime 锁的过期时间，单位毫秒
     * @Description: 加锁方法，一直阻塞尝试加锁直到成功
     * @date 2019/11/13 16:10
     * @author by mozping
     */
    public boolean lock(String key, String value, long exireptime) {

        while (true) {
            //1.执行set命令
            Boolean result = setIfAbsent(key, value, exireptime);
            //2.成功获取锁返回，否则就一直尝试
            if (Boolean.TRUE.equals(result)) {
                return true;
            }
            //3.稍微阻塞避免锁竞争和CPU空转
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                log.error("lock Exception ... ", e);
            }
        }
    }

    /**
     * @param key       加锁 key
     * @param value     加锁 value
     * @param blockTime 加锁的阻塞时间,如果blockTime时间内加锁未成功，就放弃加锁尝试,单位毫秒
     * @Description: 加锁方法，加锁阻塞一定时间
     * @date 2019/11/13 15:10
     * @author by mozping
     */
    public boolean lockTimeout(String key, String value, long blockTime) {
        return lockTimeout(key, value, defaultExpireTime, blockTime);
    }

    /**
     * @param key        加锁 key
     * @param value      加锁 value
     * @param expiretime 锁的过期时间，单位毫秒
     * @param blockTime  加锁的阻塞时间,如果blockTime时间内加锁未成功，就放弃加锁尝试,单位毫秒
     * @Description: 加锁方法，加锁阻塞一定时间
     * @date 2019/11/13 15:10
     * @author by mozping
     */
    public boolean lockTimeout(String key, String value, long expiretime, long blockTime) {
        long end = System.currentTimeMillis() + blockTime;
        while (true) {
            //1.检测是否超时，超时返回false
            long remain = end - System.currentTimeMillis();
            if (remain < 0) {
                return false;
            }
            //2.执行set命令
            Boolean result = setIfAbsent(key, value, expiretime);
            System.out.println(result);
            //3.是否成功获取锁
            if (Boolean.TRUE.equals(result)) {
                return true;
            }
            //4.sleep一小会后再重试
            try {
                Thread.sleep(Math.abs(RANDOM.nextLong()) % (remain));
            } catch (InterruptedException e) {
                log.error("lockTimeout Exception ... ", e);
            }
        }
    }


    /**
     * @param key   加锁 key
     * @param value 加锁 value
     * @Description: 尝试加锁方法
     * @date 2019/11/13 15:10
     * @author by mozping
     */
    public boolean tryLock(String key, String value) {
        return tryLock(key, value, defaultExpireTime);
    }

    /**
     * @param key        加锁 key
     * @param value      加锁 value
     * @param expiretime 锁的过期时间，单位毫秒
     * @Description: 尝试加锁方法
     * @date 2019/11/13 15:10
     * @author by mozping
     */
    public boolean tryLock(String key, String value, long expiretime) {
        //1.执行set命令
        Boolean result = setIfAbsent(key, value, expiretime);

        //2.是否成功获取锁
        return Boolean.TRUE.equals(result);
    }

    private boolean setIfAbsent(final String key, final Serializable value, final long exireptime) {
        Boolean b = (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
                RedisSerializer keySerializer = redisTemplate.getKeySerializer();
                Object obj = connection.execute("set", keySerializer.serialize(key),
                        valueSerializer.serialize(value),
                        SafeEncoder.encode("NX"),
                        SafeEncoder.encode("EX"),
                        Protocol.toByteArray(exireptime));
                return obj != null;
            }
        });
        return b;
    }


    /**
     * @param key   解锁 key
     * @param value 解锁 value
     * @Description: 解锁方法，只有当key和value匹配时，才会解锁
     * @date 2019/11/13 15:10
     * @author by mozping
     */
    public boolean unLock(String key, String value) {

        Object result = redisTemplate.execute(DEFAULT_REDIS_SCRIPT, Collections.singletonList(key), value);

        return Boolean.TRUE.equals(result);
    }


}
